/*******************************************************************************
 * Copyright (c) 2025 Eurotech and/or its affiliates and others
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *  Eurotech
 ******************************************************************************/
// Content with portions generated by generative AI platform
package org.eclipse.kura.core.crypto;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;

import org.eclipse.kura.KuraException;
import org.eclipse.kura.system.SystemService;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.Mockito;

public class CryptoServiceImplCustomKeyTest {

    @Rule
    public TemporaryFolder tempFolder = new TemporaryFolder();

    private CryptoServiceImpl cryptoService;
    private SystemService mockSystemService;
    private Path credentialDirectory;
    private String testPlaintext;
    private char[] encryptedData;
    private char[] decryptedData;

    @Before
    public void setup() {
        this.mockSystemService = Mockito.mock(SystemService.class);
        try {
            Mockito.when(this.mockSystemService.getKuraDataDirectory())
                    .thenReturn(Files.createTempDirectory(null).toString());
        } catch (IOException e) {
            throw new IllegalStateException("cannot create temporary directory");
        }
        this.cryptoService = new CryptoServiceImpl();
        this.cryptoService.setSystemService(this.mockSystemService);
    }

    @After
    public void cleanup() {
        System.clearProperty("org.eclipse.kura.crypto.secretKey");
        System.clearProperty("CREDENTIALS_DIRECTORY");
    }

    @Test
    public void loadKeyFromSystemdCredential16Bytes() {
        given16ByteCustomKey();
        givenSystemdCredentialFile("kura-encryption-secret-key");

        whenActivatingCryptoService();

        thenServiceIsActivated();
    }

    @Test
    public void loadKeyFromSystemdCredential24Bytes() {
        given24ByteCustomKey();
        givenSystemdCredentialFile("kura-encryption-secret-key");

        whenActivatingCryptoService();

        thenServiceIsActivated();
    }

    @Test
    public void loadKeyFromSystemdCredential32Bytes() {
        given32ByteCustomKey();
        givenSystemdCredentialFile("kura-encryption-secret-key");

        whenActivatingCryptoService();

        thenServiceIsActivated();
    }

    @Test
    public void loadKeyFromSystemProperty16Bytes() {
        given16ByteCustomKey();
        givenSystemPropertyKey();

        whenActivatingCryptoService();

        thenServiceIsActivated();
    }

    @Test
    public void loadKeyFromSystemProperty24Bytes() {
        given24ByteCustomKey();
        givenSystemPropertyKey();

        whenActivatingCryptoService();

        thenServiceIsActivated();
    }

    @Test
    public void loadKeyFromSystemProperty32Bytes() {
        given32ByteCustomKey();
        givenSystemPropertyKey();

        whenActivatingCryptoService();

        thenServiceIsActivated();
    }

    @Test
    public void rejectDefaultKey() {
        givenDefaultKey();
        givenSystemPropertyKey();

        whenActivatingCryptoService();

        thenServiceFallsBackToDefaultKey();
    }

    @Test
    public void rejectInvalidKeyLength15Bytes() {
        givenCustomKeyWithLength(15);
        givenSystemPropertyKey();

        whenActivatingCryptoService();

        thenServiceFallsBackToDefaultKey();
    }

    @Test
    public void rejectInvalidKeyLength17Bytes() {
        givenCustomKeyWithLength(17);
        givenSystemPropertyKey();

        whenActivatingCryptoService();

        thenServiceFallsBackToDefaultKey();
    }

    @Test
    public void rejectInvalidKeyLength20Bytes() {
        givenCustomKeyWithLength(20);
        givenSystemPropertyKey();

        whenActivatingCryptoService();

        thenServiceFallsBackToDefaultKey();
    }

    @Test
    public void rejectInvalidKeyLength31Bytes() {
        givenCustomKeyWithLength(31);
        givenSystemPropertyKey();

        whenActivatingCryptoService();

        thenServiceFallsBackToDefaultKey();
    }

    @Test
    public void useDefaultKeyWhenNoCustomKeyProvided() {
        whenActivatingCryptoService();

        thenServiceIsActivated();
    }

    @Test
    public void encryptWithSystemdCredentialKey() {
        given32ByteCustomKey();
        givenSystemdCredentialFile("kura-encryption-secret-key");
        givenPlaintext("test-data");

        whenActivatingCryptoService();
        whenEncryptingData();

        thenDataIsEncrypted();
    }

    @Test
    public void encryptAndDecryptWithCustomKey() {
        given32ByteCustomKey();
        givenSystemPropertyKey();
        givenPlaintext("test-data-to-encrypt");

        whenActivatingCryptoService();
        whenEncryptingData();
        whenDecryptingData();

        thenDecryptedDataMatchesPlaintext();
    }

    @Test
    public void systemdCredentialTakesPrecedenceOverSystemProperty() {
        given32ByteCustomKey();
        givenSystemdCredentialFile("kura-encryption-secret-key");
        givenDifferentSystemPropertyKey();

        whenActivatingCryptoService();

        thenServiceIsActivated();
    }

    private void given16ByteCustomKey() {
        this.testPlaintext = Base64.getEncoder().encodeToString("1234567890123456".getBytes(StandardCharsets.UTF_8));
    }

    private void given24ByteCustomKey() {
        this.testPlaintext = Base64.getEncoder()
                .encodeToString("123456789012345678901234".getBytes(StandardCharsets.UTF_8));
    }

    private void given32ByteCustomKey() {
        this.testPlaintext = Base64.getEncoder()
                .encodeToString("12345678901234567890123456789012".getBytes(StandardCharsets.UTF_8));
    }

    private void givenDefaultKey() {
        this.testPlaintext = "rv;ipse329183!@#";
    }

    private void givenCustomKeyWithLength(int length) {
        byte[] keyBytes = new byte[length];
        for (int i = 0; i < length; i++) {
            keyBytes[i] = (byte) i;
        }
        this.testPlaintext = Base64.getEncoder().encodeToString(keyBytes);
    }

    private void givenPlaintext(String plaintext) {
        this.testPlaintext = plaintext;
    }

    private void givenSystemdCredentialFile(String credentialName) {
        try {
            this.credentialDirectory = tempFolder.newFolder().toPath();
            Path credFile = this.credentialDirectory.resolve(credentialName);
            Files.write(credFile, this.testPlaintext.getBytes(StandardCharsets.UTF_8));
            System.setProperty("CREDENTIALS_DIRECTORY", this.credentialDirectory.toString());
        } catch (IOException e) {
            fail("Failed to create systemd credential file");
        }
    }

    private void givenSystemPropertyKey() {
        System.setProperty("org.eclipse.kura.crypto.secretKey", this.testPlaintext);
    }

    private void givenDifferentSystemPropertyKey() {
        String differentKey = Base64.getEncoder()
                .encodeToString("different-key-32-bytes-long!".getBytes(StandardCharsets.UTF_8));
        System.setProperty("org.eclipse.kura.crypto.secretKey", differentKey);
    }

    private void whenActivatingCryptoService() {
        this.cryptoService.activate();
    }

    private void whenEncryptingData() {
        try {
            this.encryptedData = this.cryptoService.encryptAes(this.testPlaintext.toCharArray());
        } catch (KuraException e) {
            fail("Encryption failed: " + e.getMessage());
        }
    }

    private void whenDecryptingData() {
        try {
            this.decryptedData = this.cryptoService.decryptAes(this.encryptedData);
        } catch (KuraException e) {
            fail("Decryption failed: " + e.getMessage());
        }
    }

    private void thenServiceIsActivated() {
        assertNotNull(this.cryptoService);
    }

    private void thenServiceFallsBackToDefaultKey() {
        assertNotNull(this.cryptoService);
    }

    private void thenDataIsEncrypted() {
        assertNotNull(this.encryptedData);
    }

    private void thenDecryptedDataMatchesPlaintext() {
        assertNotNull(this.decryptedData);
        assertArrayEquals(this.testPlaintext.toCharArray(), this.decryptedData);
    }
}
